package io.github.zerobyteword.springfasterstartup;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.framework.ProxyProcessorSupport;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.Conventions;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.lang.reflect.Modifier;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author zhaoliu
 */
public class AsyncInitBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor, PriorityOrdered, AopInfrastructureBean, BeanFactoryAware, Serializable {
    public static final String ORIGINAL_TARGET_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "originalTargetClass");
    private static final Logger log = LoggerFactory.getLogger(AsyncInitBeanPostProcessor.class);
    private final AsyncInitManager asyncInitManager;

    private ConfigurableListableBeanFactory beanFactory;

    public AsyncInitBeanPostProcessor(AsyncInitManager asyncInitManager) {
        this.asyncInitManager = asyncInitManager;

    }

    static void exposeTargetClass(ConfigurableListableBeanFactory beanFactory, @Nullable String beanName, Class<?> targetClass) {
        if (beanName != null && beanFactory.containsBeanDefinition(beanName)) {
            beanFactory.getMergedBeanDefinition(beanName).setAttribute(ORIGINAL_TARGET_CLASS_ATTRIBUTE, targetClass);
        }
    }

    static boolean isOriginalInstance(String beanName, Class<?> beanClass) {
        if (!StringUtils.hasLength(beanName) || beanName.length() != beanClass.getName().length() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX.length()) {
            return false;
        }
        return (beanName.startsWith(beanClass.getName()) && beanName.endsWith(AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX));
    }

    private static boolean hasPostConstruct(Class<?> tempClass) {
        AtomicBoolean atomicBoolean = new AtomicBoolean(false);
        do {
            ReflectionUtils.doWithLocalMethods(tempClass, method -> {
                if (method.isAnnotationPresent(PostConstruct.class)) {
                    atomicBoolean.set(true);
                }
            });
            tempClass = tempClass.getSuperclass();
        } while (tempClass != null && tempClass != Object.class && !atomicBoolean.get());
        return atomicBoolean.get();
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        asyncInitManager.startBeanInit(beanName);
        if (bean instanceof AopInfrastructureBean) {
            // Ignore AOP infrastructure such as scoped proxies.
            return bean;
        }
        if (bean instanceof Advised) {
            Advised advised = (Advised) bean;
            if (!advised.isFrozen() && isEligible(bean, beanName)) {
                // Add our local Advisor to the existing proxy's Advisor chain...
                AsyncInitBeanMethodInvoker invoker = asyncInitManager.addInvoker(bean, beanName);
                if (invoker == null) {
                    return bean;
                }
                advised.addAdvice(0, invoker);
                return bean;
            }
        }
        if (isEligible(bean, beanName)) {
            ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
            AsyncInitBeanMethodInvoker invoker = asyncInitManager.addInvoker(bean, beanName);
            if (invoker == null) {
                return bean;
            }
            proxyFactory.addAdvice(invoker);
            return proxyFactory.getProxy();
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        asyncInitManager.finishedBeanInit(beanName);
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
        if (this.beanFactory != null) {
            exposeTargetClass(this.beanFactory, beanName, bean.getClass());
        }
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.copyFrom(this);
        proxyFactory.setTarget(bean);
        proxyFactory.setProxyTargetClass(true);
        return proxyFactory;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory instanceof ConfigurableListableBeanFactory ? (ConfigurableListableBeanFactory) beanFactory : null;
    }

    protected boolean isEligible(Object bean, String beanName) {
        boolean eliglible = this.asyncInitManager.isEligibleSelf(AopUtils.getTargetClass(bean), beanName);
        if (eliglible) {
            boolean ifFinal = Modifier.isFinal(bean.getClass().getModifiers());
            if (!ifFinal && !isOriginalInstance(beanName, bean.getClass()) && hasInitMethod(bean.getClass(), bean, beanName)) {
                return true;
            }
            log.info("{}判断后不进行异步初始化", beanName);
        }
        return false;
    }

    private boolean hasInitMethod(Class<?> targetClass, Object bean, String beanName) {
        if (bean instanceof InitializingBean) {
            return true;
        }
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
        if (beanDefinition instanceof RootBeanDefinition) {
            RootBeanDefinition rbd = (RootBeanDefinition) beanDefinition;
            if (StringUtils.hasLength(rbd.getInitMethodName())) {
                return true;
            }
        }
        return asyncInitManager.doCache(targetClass, () -> hasPostConstruct(targetClass));
    }


}
